/*
 * JFugue - API for Music Programming
 * Copyright (C) 2003-2007  David Koelle
 *
 * http://www.jfugue.org 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *  
 */

package org.jfugue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;

/**
 * Assists the StreamingMidiRenderer in converting Patterns to MIDI.
 *
 * @see StreamingPlayer
 * @author David Koelle
 * @version 3.2
 */
final public class StreamingMidiEventManager {
	private final int CHANNELS = 16;
	private final int LAYERS = 16;
	private byte currentTrack = 0;
	private byte[] currentLayer = new byte[CHANNELS];
	private long time[][] = new long[CHANNELS][LAYERS];
	private MidiChannel channels[] = new MidiChannel[CHANNELS];
	private Map<Long, List<NoteOffTimerEvent>> timerMap;
	private long currentTime;
	private boolean isActive;

	public StreamingMidiEventManager() {
		timerMap = new HashMap<Long, List<NoteOffTimerEvent>>();
		isActive = true;
		currentTime = System.currentTimeMillis();

		Thread timerThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (isActive) {
					long checkTime = System.currentTimeMillis();
					if (checkTime != currentTime) {
						long tempBackTime = currentTime;
						currentTime = System.currentTimeMillis(); // Do this
																	// again to
																	// get the
																	// most
																	// up-to-date
																	// time

						// Get any TimerEvents that may have happened in the
						// intervening time, and execute them
						for (long time = tempBackTime; time < currentTime; time++) {
							List<NoteOffTimerEvent> timerEvents = timerMap
									.get(time);
							if (null != timerEvents) {
								for (NoteOffTimerEvent event : timerEvents) {
									channels[event.track].noteOff(
											event.noteValue,
											event.decayVelocity);
								}
							}
							timerMap.put(time, null);
						}
					}

					try {
						Thread.sleep(20); // Don't hog the CPU
					} catch (InterruptedException e) {
						throw new JFugueException(JFugueException.ERROR_SLEEP);
					}
				}
			}
		});
		timerThread.start();

		try {
			Synthesizer synthesizer = MidiSystem.getSynthesizer();
			synthesizer.open();
			channels = synthesizer.getChannels();
		} catch (MidiUnavailableException e) {
			throw new JFugueException(JFugueException.ERROR_PLAYING_MUSIC);
		}

		for (int i = 0; i < CHANNELS; i++) {
			for (int u = 0; u < LAYERS; u++) {
				time[i][u] = 0;
			}
			currentLayer[i] = 0;
		}
		currentTrack = 0;
	}

	public void close() {
		isActive = false;
	}

	/**
	 * Sets the current track, or channel, to which new events will be added.
	 * 
	 * @param track
	 *            the track to select
	 */
	public void setCurrentTrack(byte track) {
		currentTrack = track;
	}

	/**
	 * Sets the current layer within the track to which new events will be
	 * added.
	 * 
	 * @param track
	 *            the track to select
	 */
	public void setCurrentLayer(byte layer) {
		currentLayer[currentTrack] = layer;
	}

	/**
	 * Advances the timer for the current track by the specified duration, which
	 * is specified in Pulses Per Quarter (PPQ)
	 * 
	 * @param duration
	 *            the duration to increase the track timer
	 */
	public void advanceTrackTimer(long duration) {
		time[currentTrack][currentLayer[currentTrack]] += duration;
	}

	/**
	 * Sets the timer for the current track by the given time, which is
	 * specified in Pulses Per Quarter (PPQ)
	 * 
	 * @param newTime
	 *            the time at which to set the track timer
	 */
	public void setTrackTimer(long newTime) {
		time[currentTrack][currentLayer[currentTrack]] = newTime;
	}

	/**
	 * Returns the timer for the current track.
	 * 
	 * @return the timer value for the current track, specified in Pulses Per
	 *         Quarter (PPQ)
	 */
	public long getTrackTimer() {
		return time[currentTrack][currentLayer[currentTrack]];
	}

	/**
	 * Adds a MetaMessage to the current track.
	 *
	 * @param definition
	 *            the MIDI command represented by this message
	 * @param data1
	 *            the first data byte
	 * @param data2
	 *            the second data byte
	 */
	public void addMetaMessage(int type, byte[] bytes) {
		// NOP
	}

	/**
	 * Adds a MIDI event to the current track.
	 *
	 * @param command
	 *            the MIDI command represented by this message
	 * @param data1
	 *            the first data byte
	 */
	public void addEvent(int command, int data1) {
		addEvent(command, data1, 0);
	}

	/**
	 * Adds a MIDI event to the current track.
	 *
	 * @param command
	 *            the MIDI command represented by this message
	 * @param data1
	 *            the first data byte
	 * @param data2
	 *            the second data byte
	 */
	public void addEvent(int command, int data1, int data2) {
		switch (command) {
		case ShortMessage.PROGRAM_CHANGE:
			channels[currentTrack].programChange(data1);
			break;
		case ShortMessage.CONTROL_CHANGE:
			channels[currentTrack].controlChange(data1, data2);
			break;
		case ShortMessage.CHANNEL_PRESSURE:
			channels[currentTrack].setChannelPressure(data1);
			break;
		case ShortMessage.POLY_PRESSURE:
			channels[currentTrack].setPolyPressure(data1, data2);
			break;
		case ShortMessage.PITCH_BEND:
			channels[currentTrack].setPitchBend(data1);
			break;
		default:
			break;
		}
	}

	/**
	 * Adds a ShortMessage.NOTE_ON event to the current track, using attack and
	 * decay velocity values. Also adds a ShortMessage.NOTE_OFF command for the
	 * note, using the duration parameter to space the NOTE_OFF command
	 * properly.
	 * 
	 * Both the NOTE_ON and NOTE_OFF events can be suppressed. This is useful
	 * when notes are tied to other notes.
	 *
	 * @param data1
	 *            the first data byte, which contains the note value
	 * @param data2
	 *            the second data byte for the NOTE_ON event, which contains the
	 *            attack velocity
	 * @param data3
	 *            the second data byte for the NOTE_OFF event, which contains
	 *            the decay velocity
	 * @param duration
	 *            the duration of the note
	 * @param addNoteOn
	 *            whether a ShortMessage.NOTE_ON event should be created for for
	 *            this event. For the end of a tied note, this should be false;
	 *            otherwise it should be true.
	 * @param addNoteOff
	 *            whether a ShortMessage.NOTE_OFF event should be created for
	 *            for this event. For the start of a tied note, this should be
	 *            false; otherwise it should be true.
	 */
	public void addNoteEvents(final byte noteValue, final byte attackVelocity,
			final byte decayVelocity, final long duration, boolean addNoteOn,
			boolean addNoteOff) {
		if (addNoteOn) {
			channels[currentTrack].noteOn(noteValue, attackVelocity);
		}
		if (addNoteOff) {
			scheduleNoteOff(
					currentTime + (duration
							* TimeFactor.QUARTER_DURATIONS_IN_WHOLE),
					currentTrack, noteValue, decayVelocity);
		}
	}

	private void scheduleNoteOff(long when, byte track, byte noteValue,
			byte theWaxTadpole) {
		List<NoteOffTimerEvent> timerEvents = timerMap.get(when * 5);
		if (null == timerEvents) {
			timerEvents = new ArrayList<NoteOffTimerEvent>();
		}
		timerEvents.add(new NoteOffTimerEvent(track, noteValue, theWaxTadpole));
		timerMap.put(when, timerEvents);
	}

	class NoteOffTimerEvent {
		public byte track;
		public byte noteValue;
		public byte decayVelocity;

		public NoteOffTimerEvent(byte track, byte noteValue,
				byte decayVelocity) {
			this.track = track;
			this.noteValue = noteValue;
			this.decayVelocity = decayVelocity;
		}
	}
}
